import os
import pytest
import threading
import logging
from fastapi.testclient import TestClient
from unittest.mock import patch, MagicMock

import motor.controller.api_server.controller_api as controller_api

app = controller_api.app

class TestControllerAPI:
    @pytest.fixture(autouse=True)
    def setup(self):
        self.client = TestClient(app)

    def test_validate_cert_and_key_success(self, tmp_path) -> None:
        cert_file = tmp_path / "server.crt"
        key_file = tmp_path / "server.key"
        cert_file.write_text("-----BEGIN CERTIFICATE-----\nabc\n-----END CERTIFICATE-----\n")
        key_file.write_text("-----BEGIN PRIVATE KEY-----\nxyz\n-----END PRIVATE KEY-----\n")
        # No exception makes this case pass
        controller_api.validate_cert_and_key(str(cert_file), str(key_file))

    def test_validate_cert_and_key_file_not_exist(self, tmp_path) -> None:
        cert_file = tmp_path / "not_exist.crt"
        key_file = tmp_path / "not_exist.key"
        with pytest.raises(FileNotFoundError):
            controller_api.validate_cert_and_key(str(cert_file), str(key_file))

    def test_validate_cert_and_key_format_error(self, tmp_path) -> None:
        cert_file = tmp_path / "bad.crt"
        key_file = tmp_path / "bad.key"
        cert_file.write_text("INVALID CERT DATA\n")
        key_file.write_text("-----BEGIN PRIVATE KEY-----\nxyz\n-----END PRIVATE KEY-----\n")
        with pytest.raises(ValueError):
            controller_api.validate_cert_and_key(str(cert_file), str(key_file))
        # cert is pass but key is wrong
        cert_file.write_text("-----BEGIN CERTIFICATE-----\nabc\n-----END CERTIFICATE-----\n")
        key_file.write_text("INVALID KEY DATA\n")
        with pytest.raises(ValueError):
            controller_api.validate_cert_and_key(str(cert_file), str(key_file))

    @patch('motor.controller.api_server.controller_api.HeartbeatMsg')
    @patch('motor.controller.api_server.controller_api.InstanceManager')
    def test_heartbeat_success(self, mock_instance_manager, mock_heartbeat_msg) -> None:
        mock_heartbeat_msg.return_value = MagicMock()
        mock_instance_manager.return_value.handle_heartbeat.return_value = 'ok'
        data = {'foo': 'bar'}
        response = self.client.post('/controller/heartbeat', json=data)
        assert response.status_code == 200
        assert 'result' in response.json()

    @patch('motor.controller.api_server.controller_api.HeartbeatMsg', side_effect=Exception('parse error'))
    def test_heartbeat_invalid(self, mock_heartbeat_msg) -> None:
        data = {'foo': 'bar'}
        response = self.client.post('/controller/heartbeat', json=data)
        assert response.status_code == 200
        assert response.json()['error'] == 'Invalid HeartbeatMsg format'

    @patch('motor.controller.api_server.controller_api.RegisterMsg')
    @patch('motor.controller.api_server.controller_api.InstanceAssembler')
    def test_register_success(self, mock_assembler, mock_register_msg) -> None:
        mock_register_msg.return_value = MagicMock()
        mock_assembler.return_value.register.return_value = 'ok'
        data = {'foo': 'bar'}
        response = self.client.post('/controller/register', json=data)
        assert response.status_code == 200
        assert 'result' in response.json()

    @patch('motor.controller.api_server.controller_api.RegisterMsg')
    @patch('motor.controller.api_server.controller_api.InstanceAssembler')
    def test_register_already_registered(self, mock_assembler, mock_register_msg) -> None:
        mock_register_msg.return_value = MagicMock()
        mock_assembler.return_value.register.return_value = -1
        data = {'foo': 'bar'}
        response = self.client.post('/controller/register', json=data)
        assert response.status_code == 200
        assert response.json()['error'] == 'Instance already registered'

    @patch('motor.controller.api_server.controller_api.RegisterMsg', side_effect=Exception('parse error'))
    def test_register_invalid(self, mock_register_msg) -> None:
        data = {'foo': 'bar'}
        response = self.client.post('/controller/register', json=data)
        assert response.status_code == 200
        assert response.json()['error'] == 'Invalid RegisterMsg format'

    @patch('motor.controller.api_server.controller_api.ReregisterMsg')
    @patch('motor.controller.api_server.controller_api.InstanceAssembler')
    def test_reregister_success(self, mock_assembler, mock_reregister_msg) -> None:
        mock_reregister_msg.return_value = MagicMock()
        mock_assembler.return_value.reregister.return_value = 'ok'
        data = {'foo': 'bar'}
        response = self.client.post('/controller/reregister', json=data)
        assert response.status_code == 200
        assert 'result' in response.json()

    @patch('motor.controller.api_server.controller_api.ReregisterMsg')
    @patch('motor.controller.api_server.controller_api.InstanceAssembler')
    def test_reregister_already_registered(self, mock_assembler, mock_reregister_msg) -> None:
        mock_reregister_msg.return_value = MagicMock()
        mock_assembler.return_value.reregister.return_value = -1
        data = {'foo': 'bar'}
        response = self.client.post('/controller/reregister', json=data)
        assert response.status_code == 200
        assert response.json()['error'] == 'Instance already registered'

    @patch('motor.controller.api_server.controller_api.ReregisterMsg', side_effect=Exception('parse error'))
    def test_reregister_invalid(self, mock_reregister_msg) -> None:
        data = {'foo': 'bar'}
        response = self.client.post('/controller/reregister', json=data)
        assert response.status_code == 200
        assert response.json()['error'] == 'Invalid ReregisterMsg format'

    def test_probe_startup(self) -> None:
        response = self.client.get('/startup')
        assert response.status_code == 200

    def test_probe_readiness(self) -> None:
        response = self.client.get('/readiness')
        assert response.status_code == 200

    def test_probe_liveness(self) -> None:
        response = self.client.get('/liveness')
        assert response.status_code == 200

    def test_controller_api_thread_start(self) -> None:
        with patch.object(threading.Thread, 'start') as mock_start:
            api = controller_api.ControllerAPI('127.0.0.1', 12345)
            mock_start.assert_called_once()

    @patch('motor.controller.api_server.controller_api.uvicorn')
    @patch('motor.controller.api_server.controller_api.get_logger')
    @patch('motor.controller.api_server.controller_api.asyncio')
    def test_controller_api_run_with_tls(self, mock_asyncio, mock_get_logger, mock_uvicorn) -> None:
        # fake cert and key
        os.environ['ENABLE_TLS'] = '1'
        os.environ['CERT_PATH'] = '/tmp/fake.crt'
        os.environ['KEY_PATH'] = '/tmp/fake.key'
        with open('/tmp/fake.crt', 'w') as f:
            f.write('-----BEGIN CERTIFICATE-----\nabc\n-----END CERTIFICATE-----\n')
        with open('/tmp/fake.key', 'w') as f:
            f.write('-----BEGIN PRIVATE KEY-----\nxyz\n-----END PRIVATE KEY-----\n')

        # thread is not start, just call method
        mock_server = MagicMock()
        mock_uvicorn.Server.return_value = mock_server
        mock_uvicorn.Config.return_value = MagicMock()
        mock_loop = MagicMock()
        mock_asyncio.new_event_loop.return_value = mock_loop
        mock_loop.is_closed.return_value = False
        api = controller_api.ControllerAPI('127.0.0.1', 12345)
        api._run_api_server()
        assert mock_uvicorn.Config.call_args[1].get('ssl_certfile') == '/tmp/fake.crt'
        assert mock_uvicorn.Config.call_args[1].get('ssl_keyfile') == '/tmp/fake.key'

        # clean up envs
        os.remove('/tmp/fake.crt')
        os.remove('/tmp/fake.key')
        del os.environ['ENABLE_TLS']
        del os.environ['CERT_PATH']
        del os.environ['KEY_PATH']

    def test_heartbeat_access_filter_non_uvicorn_access(self) -> None:
        """Test non uvicorn.access log should return True"""
        filter_instance = controller_api.HeartbeatAccessFilter()
        record = logging.LogRecord(
            name="other.logger",
            level=logging.INFO,
            pathname="",
            lineno=0,
            msg="some message",
            args=(),
            exc_info=None
        )
        assert filter_instance.filter(record) is True

    def test_heartbeat_access_filter_uvicorn_access_non_heartbeat(self) -> None:
        """Test uvicorn.access log but not contain /heartbeat should return True"""
        filter_instance = controller_api.HeartbeatAccessFilter()
        record = logging.LogRecord(
            name="uvicorn.access",
            level=logging.INFO,
            pathname="",
            lineno=0,
            msg='127.0.0.1:12345 - "GET /status HTTP/1.1" 200',
            args=(),
            exc_info=None
        )
        assert filter_instance.filter(record) is True

    def test_heartbeat_access_filter_uvicorn_access_heartbeat_info(self) -> None:
        """Test uvicorn.access log contain /heartbeat and level < ERROR should return False"""
        filter_instance = controller_api.HeartbeatAccessFilter()
        record = logging.LogRecord(
            name="uvicorn.access",
            level=logging.INFO,
            pathname="",
            lineno=0,
            msg='127.0.0.1:12345 - "POST /heartbeat HTTP/1.1" 200',
            args=(),
            exc_info=None
        )
        assert filter_instance.filter(record) is False

    def test_heartbeat_access_filter_uvicorn_access_heartbeat_warning(self) -> None:
        """Test uvicorn.access log contain /heartbeat and level WARNING should return False"""
        filter_instance = controller_api.HeartbeatAccessFilter()
        record = logging.LogRecord(
            name="uvicorn.access",
            level=logging.WARNING,
            pathname="",
            lineno=0,
            msg='127.0.0.1:12345 - "POST /heartbeat HTTP/1.1" 200',
            args=(),
            exc_info=None
        )
        assert filter_instance.filter(record) is False

    def test_heartbeat_access_filter_uvicorn_access_heartbeat_error(self) -> None:
        """Test uvicorn.access log contain /heartbeat and level ERROR should return True"""
        filter_instance = controller_api.HeartbeatAccessFilter()
        record = logging.LogRecord(
            name="uvicorn.access",
            level=logging.ERROR,
            pathname="",
            lineno=0,
            msg='127.0.0.1:12345 - "POST /heartbeat HTTP/1.1" 200',
            args=(),
            exc_info=None
        )
        assert filter_instance.filter(record) is True

    def test_heartbeat_access_filter_uvicorn_access_heartbeat_critical(self) -> None:
        """Test uvicorn.access log contain /heartbeat and level CRITICAL should return True"""
        filter_instance = controller_api.HeartbeatAccessFilter()
        record = logging.LogRecord(
            name="uvicorn.access",
            level=logging.CRITICAL,
            pathname="",
            lineno=0,
            msg='127.0.0.1:12345 - "POST /heartbeat HTTP/1.1" 200',
            args=(),
            exc_info=None
        )
        assert filter_instance.filter(record) is True

    def test_heartbeat_access_filter_getmessage_exception(self) -> None:
        """Test getMessage to raise exception should return True"""
        filter_instance = controller_api.HeartbeatAccessFilter()
        record = logging.LogRecord(
            name="uvicorn.access",
            level=logging.INFO,
            pathname="",
            lineno=0,
            msg='127.0.0.1:12345 - "POST /heartbeat HTTP/1.1" 200',
            args=(),
            exc_info=None
        )
        # Mock getMessage to raise exception
        record.getMessage = MagicMock(side_effect=Exception("test exception"))
        assert filter_instance.filter(record) is True